﻿using System;
using System.Collections.Generic;
using System.Linq;
using LINQPad.Extensibility.DataContext;
using LINQPad.OpenAccess.Converters;
using LINQPad.OpenAccess.Helpers;
using LINQPad.OpenAccess.Infrastructure;
using LINQPad.OpenAccess.Metadata;
using LINQPad.OpenAccess.Metadata.Reflection;
using Telerik.OpenAccess.Metadata;

namespace LINQPad.OpenAccess.Services
{
    /// <summary>
    /// <see cref="IDataContextSchemaProvider"/>
    /// </summary>
    public partial class DataContextSchemaProvider : IDataContextSchemaProvider
    {
        #region Private

        #region - Vars

        private readonly IReflectionService _reflectionService;
        private readonly AssociationMultiplicityToExplorerIconConverter _multiplicityIconConverter;

        #endregion

        #endregion

        #region Protected

        #region - Functions

        /// <summary>
        /// Creates the child items for the specified entity type.
        /// </summary>
        /// <param name="context">The context.</param>
        /// <param name="entityType">Type of the entity.</param>
        /// <returns>
        /// The items.
        /// </returns>
        protected virtual IEnumerable<ExplorerItem> CreateChildItemsForEntityType( SchemaBuildContext context, MetaPersistentType entityType )
        {
            // create primitive members
            foreach ( var memberItem in entityType.Members
                                                 .OfType<MetaPrimitiveMember>( )
                                                 .Where( ShouldCreatePrimitiveMemberItem )
                                                 .Select( p => CreatePrimitiveMemberItem( context, p ) ) )
                yield return memberItem;


            // create navigation members
            foreach ( var memberItem in entityType.Members
                                                 .OfType<MetaNavigationMember>( )
                                                 .Where( ShouldCreateNavigationMemberItem )
                                                 .Select( p => CreateNavigationMemberItem( context, p ) ) )
                yield return memberItem;
        }
        /// <summary>
        /// Creates the item for the specified entity set.
        /// </summary>
        /// <param name="entitySet">The entity set.</param>
        /// <returns>
        /// The item.
        /// </returns>
        protected virtual ExplorerItem CreateEntitySetItem( SchemaBuildContext context, EntitySetInfo entitySet, MetaPersistentType entityType )
        {
            ExplorerItem item;
            if ( !context.TryGetEntitySetItem( entitySet, entityType, out item ) )
            {
                item = new ExplorerItem( entitySet.Name, ExplorerItemKind.QueryableObject, ExplorerIcon.Table )
                {
                    IsEnumerable = true,
                    ToolTipText = DataContextDriver.FormatTypeName( entitySet.EntitySetType, false ),
                    Tag = entityType
                };

                context.TryAddEntitySetItem( entitySet, entityType, item );
            }
            return item;
        }
        /// <summary>
        /// Creates the item for the specified navigation member.
        /// </summary>
        /// <param name="context">The context.</param>
        /// <param name="member">The member.</param>
        /// <returns>
        /// The item.
        /// </returns>
        protected virtual ExplorerItem CreateNavigationMemberItem( SchemaBuildContext context, MetaNavigationMember member )
        {
            ExplorerItem item;
            if ( !context.TryGetItemForMember( member, out item ) )
            {
                var oppositeMember = member.GetOppositeMember( );

                item = new ExplorerItem
                                (
                                    member.PropertyName
                                    , member.Multiplicity == Multiplicity.Many ? ExplorerItemKind.CollectionLink : ExplorerItemKind.ReferenceLink
                                    , this.GetExplorerIconForNavigationMember( member )
                                )
                {
                    ToolTipText = DataContextDriver.FormatTypeName( Type.GetType( member.MemberType.FullName ), true ),
                    Tag = member
                };
                // add to context
                context.TryAddItemForMember( member, item );

                // set hyperlink
                ExplorerItem hyperlinkTarget;
                if ( oppositeMember == null ) // try to set link to target entity set.
                {
                    var targetType = member.Association.SourceEnd == member
                                            ? ( MetaPersistentType ) member.Association.Target
                                            : ( MetaPersistentType ) member.Association.Source;
                    if ( !context.TryGetEntitySetItemForType( targetType, out hyperlinkTarget ) )
                        hyperlinkTarget = null;
                }
                else // set link to opposite navigation member
                {
                    if ( !context.TryGetItemForMember( oppositeMember, out hyperlinkTarget ) ) // create item for opposite member if it doesn't exists yet.
                        hyperlinkTarget = this.CreateNavigationMemberItem( context, oppositeMember );
                }
                item.HyperlinkTarget = hyperlinkTarget;

            }
            return item;
        }
        /// <summary>
        /// Creates the item for the specified primitive member.
        /// </summary>
        /// <param name="context">The context.</param>
        /// <param name="member">The member.</param>
        /// <returns>
        /// The item.
        /// </returns>
        protected virtual ExplorerItem CreatePrimitiveMemberItem( SchemaBuildContext context, MetaPrimitiveMember member )
        {
            ExplorerItem item;
            if ( !context.TryGetItemForMember( member, out item ) )
            {
                item = new ExplorerItem
                                (
                                    String.Format
                                            (
                                                "{0} ({1})"
                                                , member.PropertyName
                                                , DataContextDriver.FormatTypeName( Type.GetType( member.MemberType.FullName ), false )
                                            )
                                    , ExplorerItemKind.Property
                                    , member.IsIdentity ? ExplorerIcon.Key : ExplorerIcon.Column
                                )
                {
                    Tag = member
                };
                context.TryAddItemForMember( member, item );
            }
            return item;
        }
        /// <summary>
        /// Creates the schema build context.
        /// </summary>
        /// <param name="contextType">Type of the context.</param>
        /// <param name="contextMetadata">The metadata of the context.</param>
        /// <returns>
        /// The schema build context.
        /// </returns>
        protected SchemaBuildContext CreateSchemaBuildContext( Type contextType, MetadataContainer contextMetadata )
        {
            return new SchemaBuildContext( contextType, contextMetadata );
        }
        /// <summary>
        /// Ensures that the specified type is a valid open access context type.
        /// </summary>
        /// <param name="contextType">Type of the context.</param>
        /// <exception cref="System.InvalidOperationException">If the type is invalid.</exception>
        protected void EnsureIsValidContextType( Type contextType )
        {
            Guard.ArgumentNotNull( contextType, "contextType" );
            if ( !this._reflectionService.IsContextType( contextType ) )
                throw new InvalidOperationException( String.Format( "The type '{0}' is no valid OpenAccess context type !", contextType.FullName ) );
        }
        /// <summary>
        /// Gets the explorer icon for the specified navigation member.
        /// </summary>
        /// <param name="navigationMember">The navigation member.</param>
        /// <returns>
        /// The icon.
        /// </returns>
        protected virtual ExplorerIcon GetExplorerIconForNavigationMember( MetaNavigationMember navigationMember )
        {
            var multiplicity = navigationMember.Multiplicity;
            Multiplicity oppositeMultiplicity;
            var oppositeMember = navigationMember.GetOppositeMember( );
            if ( oppositeMember == null )
            {
                // ToDo: infer from association
                oppositeMultiplicity = Multiplicity.Many;
            }
            else
                oppositeMultiplicity = oppositeMember.Multiplicity;

            return this.MultiplicityIconConverter.ToExplorerIcon( multiplicity, oppositeMultiplicity );
        }
        /// <summary>
        /// Determines whether an item should be created for the specified navigation member.
        /// </summary>
        /// <param name="member">The member.</param>
        /// <returns>
        ///   <c>true</c> if an item should be created; otherwise, <c>false</c>.
        /// </returns>
        protected virtual bool ShouldCreateNavigationMemberItem( MetaNavigationMember member )
        {
            return member != null
                    && member.MemberAccessModifier == MemberAccessModifier.Public;
        }
        /// <summary>
        /// Determines whether an item should be created for the specified primitive member.
        /// </summary>
        /// <param name="member">The member.</param>
        /// <returns>
        ///   <c>true</c> if an item should be created; otherwise, <c>false</c>.
        /// </returns>
        protected virtual bool ShouldCreatePrimitiveMemberItem( MetaPrimitiveMember member )
        {
            return member != null
                    && member.MemberAccessModifier == MemberAccessModifier.Public;
        }

        #endregion

        #endregion

        #region Public

        #region - Constructors

        /// <summary>
        /// Initializes a new instance of the <see cref="DataContextSchemaProvider" /> class.
        /// </summary>
        /// <param name="reflectionService">The reflection service.</param>
        /// <param name="multiplicityIconConverter">The multiplicity icon converter.</param>
        public DataContextSchemaProvider( IReflectionService reflectionService
                                            , AssociationMultiplicityToExplorerIconConverter multiplicityIconConverter )
        {
            Guard.ArgumentNotNull( reflectionService, "reflectionService" );
            Guard.ArgumentNotNull( multiplicityIconConverter, "multiplicityIconConverter" );

            this._reflectionService = reflectionService;
            this._multiplicityIconConverter = multiplicityIconConverter;
        }

        #endregion

        #region - Properties

        /// <summary>
        /// Gets the multiplicity icon converter.
        /// </summary>
        /// <value>
        /// The multiplicity icon converter.
        /// </value>
        public AssociationMultiplicityToExplorerIconConverter MultiplicityIconConverter
        {
            get
            {
                return this._multiplicityIconConverter;
            }
        }

        #endregion

        #endregion

        #region IDataContextSchemaProvider Members

        /// <summary>
        /// Gets the schema for the specified openaccess context.
        /// </summary>
        /// <param name="contextType">Type of the context.</param>
        /// <param name="contextMetadata">The metadata of the context.</param>
        /// <returns>
        /// The schema.
        /// </returns>
        public List<ExplorerItem> GetSchema( Type contextType, MetadataContainer contextMetadata )
        {
            this.EnsureIsValidContextType( contextType );
            Guard.ArgumentNotNull( contextMetadata, "contextMetadata" );

            var buildContext = this.CreateSchemaBuildContext( contextType, contextMetadata );

            var persistentTypes = contextMetadata.PersistentTypes;
            var entitySetItems = this._reflectionService
                                     .GetEntitySetsFromContext( contextType )
                                     .OrderBy( e => e.Name )
                                     .Join
                                        (
                                            persistentTypes
                                            , e => e.EntityType.FullName
                                            , p => p.FullName
                                            , ( e, p ) => this.CreateEntitySetItem( buildContext, e, p )
                                        )
                                     .ToList( );

            foreach ( var item in entitySetItems )
                item.Children = this.CreateChildItemsForEntityType( buildContext, ( MetaPersistentType ) item.Tag )
                                    .OrderBy( e => e.Kind )
                                    .ThenByDescending( e => e.Icon )
                                    .ThenBy( e => e.Text )
                                    .ToList( );

            return entitySetItems;
        }

        #endregion
    }
}
